﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using ActivateYourGlutes.Data;
using ActivateYourGlutes.Infrastructure;
using ActivateYourGlutes.Services.Model;
using AutoMapper;
using Goal = ActivateYourGlutes.Data.Model.Goal;

namespace ActivateYourGlutes.Services.Implementation
{
    public class BodyProvider : ProviderBase<Data.Model.Body>, IBodyProvider
    {
        private const int KeyIndicatorResultsRange = 20;

        public BodyProvider(IEntityModelFactory factory)
            : base(factory)
        {
            
        }

        /// <summary>
        /// Returns a list of measurement types that are selected by default (for example when viewing graphs)
        /// </summary>
        public List<MeasurementType> DefaultMeasurementTypes
        {
            get
            {
                var viewTypes = GetRepository<Data.Model.MeasurementType>()
                    .EagerlyLoadedEntities("Owner")
                    .Where(mt => mt.DisplayOrder == 1 || mt.DisplayOrder == 2).ToList();

                return Mapper.Map<List<Data.Model.MeasurementType>, List<MeasurementType>>(viewTypes);
            }
        }

        /// <summary>
        /// Returns a series of values for a users selecetd primary measurement type
        /// </summary>
        public Series PrimaryIndicatorSeries(Account user)
        {
            return SeriesForIndicator(user, user.PrimaryMeasurementType, false);
        }

        /// <summary>
        /// Returns a series of values for a users selected secondary measurement type
        /// </summary>
        public Series SecondaryIndicatorSeries(Account user)
        {
            return SeriesForIndicator(user, user.SecondaryMeasurementType, false);
        }

        /// <summary>
        /// Returns a series of measurement values for the specified measurementt type
        /// </summary>
        public Series SeriesForIndicator(Account user, MeasurementType type, bool padding)
        {
            var results = Repository.EagerlyLoadedEntities("Measurements", "Measurements.MeasurementType").
                Where(b => b.Measurements.Where(m => m.MeasurementType.MeasurementTypeId == type.MeasurementTypeId).Any()).
                Where(b => b.Account.AccountId == user.AccountId).
                OrderByDescending(b => b.EntryDate).Take(KeyIndicatorResultsRange).ToList();
            results.Reverse();

            var series = new Series { Title = type.Description };
            SeriesValuePair lastAdded = null;
            DateTime? lastDate = null;
            foreach (var body in results)
            {
                var internalMeasurement =
                    body.Measurements.Where(m => m.MeasurementType.MeasurementTypeId == type.MeasurementTypeId).First();
                var measurement = Mapper.Map<Data.Model.Measurement, Measurement>(internalMeasurement);
                var value = measurement.LocalisedValue;
                var xLabel = user.FormatDate(body.EntryDate);
                // If we have two or more measurements for the same day we only include the last one on a graph. We can simply look
                // at the last element of the series as we've sorted the results by date.
                if (lastAdded != null && lastAdded.X == xLabel)
                {
                    series.Values.Last().Y = value;
                }
                else
                {
                    if (lastDate != null && padding)
                    {
                        var startDate = new DateTime(lastDate.Value.Year, lastDate.Value.Month, lastDate.Value.Day);
                        startDate = startDate.AddDays(1);
                        var endDate = new DateTime(body.EntryDate.Year, body.EntryDate.Month, body.EntryDate.Day);
                        while(startDate < endDate)
                        {
                            var paddingPair = new SeriesValuePair {X = user.FormatDate(startDate)};
                            series.Values.Add(paddingPair);
                            startDate = startDate.AddDays(1);
                        }
                    }
                    var seriesValuePair = new SeriesValuePair
                                              {
                                                  XDate = new DateTime(body.EntryDate.Year, body.EntryDate.Month, body.EntryDate.Day),
                                                  X = xLabel,
                                                  Y = value
                                              };
                    series.Values.Add(seriesValuePair);
                    lastAdded = seriesValuePair;
                    lastDate = body.EntryDate;
                }
            }
            return series;
        }

        /// <summary>
        /// Returns the total number of body measurement sets in the account
        /// </summary>
        public int TotalBodiesForAccount(Account user)
        {
            return Repository.Entities.Where(b => b.Account.AccountId == user.AccountId).Count();
        }

        /// <summary>
        /// Returns a paged view of logged body measurements for a given user
        /// </summary>
        public IEnumerable<Body> BodiesForAccount(Account user, int offset, int pageSize)
        {
            var bodies= Repository.EagerlyLoadedEntities("Account", "Measurements", "Measurements.MeasurementType").
                Where(b => b.Account.AccountId == user.AccountId).
                OrderByDescending(b => b.EntryDate).
                Skip(offset).
                Take(pageSize).
                ToList();
            return Mapper.Map<List<Data.Model.Body>,List<Body>>(bodies);
        }

        /// <summary>
        /// Adds a body measurement set to the database
        /// </summary>
        public void Add(Body bodyToAdd)
        {
            string description = null;
            const string title = "Body measurements logged";

            var measurementTypeRepository = GetRepository<Data.Model.MeasurementType>();
            var body = new Data.Model.Body();
            body.EntryDate = bodyToAdd.EntryDate;
            body.AccountId = bodyToAdd.Account.AccountId;
            foreach(var measurementToAdd in bodyToAdd.Measurements)
            {
                var localisedMeasurementToAdd = measurementToAdd;
                var measurement = new Data.Model.Measurement
                                      {
                                          Value = measurementToAdd.Value,
                                          MeasurementType = measurementTypeRepository.Entities.Where(
                                              mt =>
                                              mt.MeasurementTypeId == localisedMeasurementToAdd.MeasurementType.MeasurementTypeId)
                                              .First()
                                      };
                // You need to attach a full type here rather than a reference as the validation in the measurement
                // depends on having access to this due to its metadata nature
                body.Measurements.Add(measurement);
            }

            var errors = body.Validate();
            if (errors != null && errors.Count > 0)
                throw new ValidationException(errors);

            if (bodyToAdd.Account.PrimaryMeasurementType != null)
            {
                var kiText = bodyToAdd.Account.PrimaryMeasurementType.Description;
                var kiValue =
                    bodyToAdd.Measurements.Where(
                        m =>
                        m.MeasurementType.MeasurementTypeId == bodyToAdd.Account.PrimaryMeasurementType.MeasurementTypeId).
                        FirstOrDefault();
                if (kiValue != null)
                    description = String.Format("{0} recorded as {1}", kiText, kiValue.Value);
            }
            
            // Log the new body measurement as an activity
            var activity = new Data.Model.Activity
                               {
                                   //Account = body.Account,
                                   ActivityDate = DateTime.Now,
                                   Body = body,
                                   Title = title,
                                   Description = description,
                                   AccountId = bodyToAdd.Account.AccountId
                               };

            Repository.Create(body);
            GetRepository<Data.Model.Activity>().Create(activity);            
            Repository.SaveChanges();
            // I need to improve this so that it is included in the transaction above.
            ProcessAchievedGoals(body);
        }

        /// <summary>
        /// Updates a body measurement set in the database
        /// </summary>
        public void Update(Account updatingUser, Body updateFromBody)
        {
            var bodyToUpdate = Repository.EagerlyLoadedEntities("Measurements").Where(b => b.BodyId == updateFromBody.BodyId).First();
            if (bodyToUpdate.Account.AccountId != updatingUser.AccountId)
                throw new AuthorizationException("Attempt to edit an item that the current user does not have access to");

            var measurementTypeRepository = GetRepository<Data.Model.MeasurementType>();
            var measurementRepository = GetRepository<Data.Model.Measurement>();
            const string title = "Existing body measurement updated";
            bodyToUpdate.EntryDate = updateFromBody.EntryDate;
            var measurementTypes = measurementTypeRepository.Entities.ToList();

            // Update existing measurements and add any new ones
            foreach (var measurement in updateFromBody.Measurements)
            {
                var closureMeasurement = measurement;
                var existingMeasurement =
                    bodyToUpdate.Measurements.Where(
                        m => m.MeasurementType.MeasurementTypeId == closureMeasurement.MeasurementType.MeasurementTypeId).
                        FirstOrDefault();
                if (existingMeasurement != null)
                    existingMeasurement.Value = measurement.Value;
                else
                {
                    var type =
                        measurementTypes.Where(
                            mt => mt.MeasurementTypeId == closureMeasurement.MeasurementType.MeasurementTypeId).First();
                    bodyToUpdate.Measurements.Add(new Data.Model.Measurement { Body = bodyToUpdate, Value = measurement.Value, MeasurementType = type });
                }                
            }

            // Remove measurements that have been removed in the source
            var toDeleteSet =
                bodyToUpdate.Measurements.Where(
                    m =>
                    !updateFromBody.Measurements.Where(
                         m2 => m2.MeasurementType.MeasurementTypeId == m.MeasurementType.MeasurementTypeId).Any()).ToList();
            foreach(var toDelete in toDeleteSet)
                measurementRepository.Delete(toDelete);

            var errors = bodyToUpdate.Validate();
            if (errors != null && errors.Count > 0)
                throw new ValidationException(errors);

            var activity = new Data.Model.Activity
                               {
                                   ActivityDate = DateTime.Now,
                                   Body = bodyToUpdate,
                                   Title = title,
                                   AccountId = bodyToUpdate.Account.AccountId
                               };
            GetRepository<Data.Model.Activity>().Create(activity);
            Repository.SaveChanges();
            // Needs improving to take part in above transaction
            ProcessAchievedGoals(bodyToUpdate);
        }

        /// <summary>
        /// Deletes a body measurement set from the database
        /// </summary>
        public void Delete(Account deletedBy, int bodyId)
        {
            var activitiesRepository = GetRepository<Data.Model.Activity>();
            var measurementRepository = GetRepository<Data.Model.Measurement>();

            var body = Repository.EagerlyLoadedEntities("Measurements", "Account").Where(b => b.BodyId == bodyId).FirstOrDefault();
            if (body == null)
                return;
            if (body.Account.AccountId != deletedBy.AccountId)
                throw new AuthorizationException(
                    "Attempt to delete an item that does not belong to the current user");

            var activitiesForBody = activitiesRepository.Entities.Where(a => a.Body.BodyId == body.BodyId).ToList();
            foreach (var activityForBody in activitiesForBody)
            {
                if (activityForBody != null)
                    activitiesRepository.Delete(activityForBody);
            }
            var measurements = body.Measurements.ToList();
            foreach (var measurement in measurements)
                measurementRepository.Delete(measurement);
            Repository.Delete(body);
            Repository.SaveChanges();
        }

        /// <summary>
        /// Returns a body measurement set based on an id
        /// </summary>
        public Body GetBodyById(Account requestingAccount, int id)
        {
            var body = Repository.EagerlyLoadedEntities("Measurements", "Measurements.MeasurementType", "Account").Where(b => b.BodyId == id).FirstOrDefault();
            if (body.Account.AccountId != requestingAccount.AccountId)
                throw new AuthorizationException("Attempt to edit an item that the current user does not have access to");
            return Mapper.Map<Data.Model.Body, Body>(body);
        }

        /// <summary>
        /// Gets the latest measurements by combining the latest measurement of each type for the account
        /// </summary>
        public Body ConsolidatedBody(Account account)
        {
            var measurementRepository = GetRepository<Data.Model.Measurement>();
            var result = new Body {EntryDate = DateTime.Now};
            var measurementTypes = GetMeasurementTypes(account);
            foreach (var type in measurementTypes)
            {
                var measurement =
                    measurementRepository.EagerlyLoadedEntities("MeasurementType").
                        Where(m => m.MeasurementType.MeasurementTypeId == type.MeasurementTypeId &&
                                   m.Body.Account.AccountId == account.AccountId).
                        OrderByDescending(m => m.Body.EntryDate).
                        FirstOrDefault();

                if (measurement != null)
                {
                    result.Measurements.Add(Mapper.Map<Data.Model.Measurement, Measurement>(measurement));
                }
            }
            if (!result.Measurements.Any())
                result = null;
            return result;
        }

        /// <summary>
        /// Returns all body measurement sets for a given account
        /// </summary>
        public IEnumerable<Body> All(Account forAccount)
        {
            var bodies = Repository.EagerlyLoadedEntities("Measurements", "Measurements.MeasurementType").Where(b => b.Account.AccountId == forAccount.AccountId).OrderBy(b => b.EntryDate).ToList();
            return Mapper.Map<List<Data.Model.Body>,List<Body>>(bodies);
        }

        /// <summary>
        /// Writes all body measurement sets as XML to the supplied stream
        /// </summary>
        public void ToXml(Account forAccount, Stream stream)
        {
            var writer = new XmlTextWriter(stream, Encoding.Unicode);
            writer.WriteStartDocument();
            writer.WriteStartElement("results");
            var bodies = All(forAccount);
            foreach(var body in bodies)
            {
                writer.WriteStartElement("item");
                writer.WriteAttributeString("entryDate", body.EntryDate.ToString("yyyy-MM-dd hh:mm:ss"));
                
                foreach(var measurement in body.Measurements)
                {
                    writer.WriteStartElement("measurement");
                    writer.WriteAttributeString("description", measurement.MeasurementType.Description);
                    writer.WriteAttributeString("value", measurement.LocalisedValue.ToString());
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
            writer.WriteEndElement();
            writer.WriteEndDocument();
            writer.Flush();
        }

        private void ProcessAchievedGoals(Data.Model.Body body)
        {
            var anyUpdated = false;
            var goalRepository = GetRepository<Goal>();

            var activeGoals = goalRepository.Entities.Where(g => g.Account.AccountId == body.AccountId && g.Active && g.AchievedOn == null);
            foreach(var goal in activeGoals)
            {
                var localisedGoal = goal;

                var latestMatchingBody =
                    Repository.EagerlyLoadedEntities("Measurements").Where(
                        b =>
                        b.Measurements.Where(
                            m => m.MeasurementType.MeasurementTypeId == localisedGoal.MeasurementType.MeasurementTypeId).Any() &&
                        b.Account.AccountId == body.AccountId).
                        OrderByDescending(b => b.EntryDate).Take(1).FirstOrDefault();
                
                if (latestMatchingBody == null) continue;

                var comparison =
                    latestMatchingBody.Measurements.Where(
                        m => m.MeasurementType.MeasurementTypeId == localisedGoal.MeasurementType.MeasurementTypeId).First();
                var comparisonValue = comparison.Value;
                var targetValue = goal.TargetValue;
                var succeeded = (goal.DecreaseTo && comparisonValue <= targetValue) ||
                                (!goal.DecreaseTo && comparisonValue >= targetValue);
                if (succeeded)
                {
                    goal.AchievedOn = DateTime.Now;
                    anyUpdated = true;
                }
            }

            if (anyUpdated)
                Repository.SaveChanges();
        }
    }
}